﻿//-----------------------------------------------------------------------
// <copyright company="www.3WayWebdesign.de">
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
//
// <author>Michael Morbach</author>
// <email>Michael@3waywebdesign.de</email>
// <web>http://www.3waywebdesign.de</web>
//-----------------------------------------------------------------------

using System;
using System.ComponentModel;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using Microsoft.Windows.Shell.Standard;
using NLog;
using Tww.Wpf.Controls.Controls.Menus.ExpandMenu;
using Tww.Wpf.Controls.Controls.Shared;
using Tww.Wpf.Controls.Controls.Shared.Enums;
using Tww.Wpf.Controls.Controls.Shared.Helper;
using Tww.Wpf.Controls.Controls.Shared.Interfaces;
using Tww.Wpf.Controls.Controls.Windows;

namespace Tww.Wpf.Controls.Controls.Flyouts
{
    /// <summary>
    /// A sliding panel control that is hosted in a MetroWindow via a FlyoutsControl.
    /// <see cref="MetroWindow"/>
    /// <seealso cref="FlyoutContainer"/>
    /// </summary>
    [TemplatePart(Name = PART_MAXButton, Type = typeof(Button))]
    [TemplatePart(Name = PART_MINButton, Type = typeof(Button))]
    [TemplatePart(Name = PART_CloseButton, Type = typeof(Button))]
    [TemplatePart(Name = PART_Content, Type = typeof(ContentPresenter))]
    public class Flyout : ContentControl, ISizeableElement
    {
        private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

        #region  - Felder (öffentlich) -

        public static readonly DependencyProperty AnimateOnPositionChangeProperty =
            DependencyProperty.Register("AnimateOnPositionChange", typeof(bool), typeof(Flyout),
                new PropertyMetadata(true));

        public static readonly DependencyProperty CloseCommandProperty =
            DependencyProperty.RegisterAttached("CloseCommand", typeof(ICommand), typeof(Flyout),
                new UIPropertyMetadata(null));

        public static readonly DependencyProperty ExternalCloseButtonProperty =
            DependencyProperty.Register("ExternalCloseButton", typeof(MouseButton), typeof(Flyout),
                new PropertyMetadata(MouseButton.Left));

        public static readonly DependencyProperty ElementSizeProperty =
            DependencyProperty.Register("ElementSize", typeof(ElementSize), typeof(Flyout),
                new FrameworkPropertyMetadata(ElementSize.Maximized,
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, FlyoutSizePropertyChangedCallback));

        private static void FlyoutSizePropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
        {
            var flyout = (Flyout)dependencyObject;
            var oldState = (ElementSize)dependencyPropertyChangedEventArgs.OldValue;
            var newState = (ElementSize)dependencyPropertyChangedEventArgs.NewValue;

            if (oldState != newState)
            {
                switch (newState)
                {
                    case ElementSize.Maximized:
                        VisualStateManager.GoToState(flyout, VisualStates.StateMaximized, true);
                        break;
                    case ElementSize.Minimized:
                        VisualStateManager.GoToState(flyout, VisualStates.StateMinimized, true);
                        break;
                    default:
                    case ElementSize.Collapsed:
                        VisualStateManager.GoToState(flyout, VisualStates.StateCollapsed, true);
                        break;
                }
            }
        }

        public static readonly DependencyProperty IsModalProperty = DependencyProperty.Register("IsModal", typeof(bool),
            typeof(Flyout));

        public static readonly DependencyProperty IsPinnedProperty = DependencyProperty.Register("IsPinned",
            typeof(bool), typeof(Flyout), new PropertyMetadata(true));

        public static readonly DependencyProperty MaximizeCommandProperty =
            DependencyProperty.RegisterAttached("MaximizeCommand", typeof(ICommand), typeof(Flyout),
                new UIPropertyMetadata(null));

        public static readonly DependencyProperty MaximizedContentTemplateProperty =
            DependencyProperty.Register("MaximizedContentTemplate",
                typeof(DataTemplate), typeof(Flyout));

        public static readonly DependencyProperty MinimizeCommandProperty =
            DependencyProperty.RegisterAttached("MinimizeCommand", typeof(ICommand), typeof(Flyout),
                new UIPropertyMetadata(null));

        public static readonly DependencyProperty MinimizedContentTemplateProperty =
            DependencyProperty.Register("MinimizedContentTemplate",
                typeof(DataTemplate), typeof(Flyout));

        public static readonly DependencyProperty PositionProperty = DependencyProperty.Register("Position",
            typeof(Position), typeof(Flyout), new PropertyMetadata(Position.Left, PositionChanged));

        #endregion

        #region  - Felder (privat) -

        private ContentPresenter content;
        private EasingDoubleKeyFrame hideFrame;
        private EasingDoubleKeyFrame hideFrameY;
        private Storyboard hideStoryboard;
        private Grid root;
        private EasingDoubleKeyFrame showFrame;
        private EasingDoubleKeyFrame showFrameY;
        private Storyboard showStoryBoard;

        #endregion

        #region  - Konstruktoren -

        static Flyout()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(Flyout), new FrameworkPropertyMetadata(typeof(Flyout)));
        }

        #endregion

        #region  - Konstruktoren -

        public Flyout()
        {
            Loaded += (sender, args) => UpdateFlyoutTheme();
        }

        #endregion

        #region  - Indexer und Eigenschaften (öffentlich) -

        public bool IsFlyoutVisible
        {
            get { return ElementSize != ElementSize.Collapsed; }
        }

        /// <summary>
        ///     Gets/sets the template for the minimized Flyout visual.
        /// </summary>
        public DataTemplate MinimizedContentTemplate
        {
            get { return (DataTemplate)GetValue(MinimizedContentTemplateProperty); }
            set { SetValue(MinimizedContentTemplateProperty, value); }
        }

        /// <summary>
        ///     Gets/sets the template for the maximized Flyout visual.
        /// </summary>
        public DataTemplate MaximizedContentTemplate
        {
            get { return (DataTemplate)GetValue(MaximizedContentTemplateProperty); }
            set { SetValue(MaximizedContentTemplateProperty, value); }
        }

        /// <summary>
        /// Gets or sets the state of the flyout.
        /// </summary>
        /// <value>
        /// The state of the flyout.
        /// </value>
        public ElementSize ElementSize
        {
            get { return (ElementSize)GetValue(ElementSizeProperty); }
            set { SetValue(ElementSizeProperty, value); }
        }

        /// <summary>
        /// An ICommand that executes when the flyout's maximize button is clicked.
        /// </summary>
        public ICommand MaximizeCommand
        {
            get { return (ICommand)GetValue(MaximizeCommandProperty); }
            set { SetValue(MaximizeCommandProperty, value); }
        }

        /// <summary>
        /// An ICommand that executes when the flyout's minimize button is clicked.
        /// </summary>
        public ICommand MinimizeCommand
        {
            get { return (ICommand)GetValue(MinimizeCommandProperty); }
            set { SetValue(MinimizeCommandProperty, value); }
        }

        /// <summary>
        /// An ICommand that executes when the flyout's close button is clicked.
        /// </summary>
        public ICommand CloseCommand
        {
            get { return (ICommand)GetValue(CloseCommandProperty); }
            set { SetValue(CloseCommandProperty, value); }
        }

        public void MinimizeFlyout()
        {
            if (this.ElementSize != ElementSize.Minimized)
            {
                Logger.Trace("Minimizing the Flyout");
                hideStoryboard.Completed -= OnHideCompletedCollapsedActions;
                hideStoryboard.Completed += OnHideCompletedMinimizedActions;
                hideStoryboard.Completed -= OnHideCompletedMaximizedActions;

                if (hideStoryboard != null)
                {
                    hideStoryboard.Begin(root);
                }
            }
        }

        public void MaximizeFlyout()
        {
            if (this.ElementSize != ElementSize.Maximized)
            {
                Logger.Trace("Maximizing the Flyout");
                hideStoryboard.Completed -= OnHideCompletedCollapsedActions;
                hideStoryboard.Completed -= OnHideCompletedMinimizedActions;
                hideStoryboard.Completed += OnHideCompletedMaximizedActions;

                if (hideStoryboard != null)
                {
                    hideStoryboard.Begin(root);
                }
            }
        }

        public void CloseFlyout()
        {
            if (this.ElementSize != ElementSize.Collapsed)
            {
                Logger.Trace("Closing the Flyout");
                hideStoryboard.Completed += OnHideCompletedCollapsedActions;
                hideStoryboard.Completed -= OnHideCompletedMinimizedActions;
                hideStoryboard.Completed -= OnHideCompletedMaximizedActions;

                if (hideStoryboard != null)
                {
                    hideStoryboard.Begin(root);
                }
            }
        }

        /// <summary>
        /// Gets/sets whether this flyout uses the open/close animation when changing the <see cref="Position"/> property. (default is true)
        /// </summary>
        public bool AnimateOnPositionChange
        {
            get { return (bool)GetValue(AnimateOnPositionChangeProperty); }
            set { SetValue(AnimateOnPositionChangeProperty, value); }
        }

        /// <summary>
        /// Gets/sets whether this flyout stays open when the user clicks outside of it.
        /// </summary>
        public bool IsPinned
        {
            get { return (bool)GetValue(IsPinnedProperty); }
            set { SetValue(IsPinnedProperty, value); }
        }

        /// <summary>
        /// Gets/sets the mouse button that closes the flyout on an external mouse click.
        /// </summary>
        public MouseButton ExternalCloseButton
        {
            get { return (MouseButton)GetValue(ExternalCloseButtonProperty); }
            set { SetValue(ExternalCloseButtonProperty, value); }
        }

        /// <summary>
        /// Gets/sets whether this flyout is modal.
        /// </summary>
        public bool IsModal
        {
            get { return (bool)GetValue(IsModalProperty); }
            set { SetValue(IsModalProperty, value); }
        }

        /// <summary>
        /// Gets/sets this flyout's position in the FlyoutsControl/MetroWindow.
        /// </summary>
        public Position Position
        {
            get { return (Position)GetValue(PositionProperty); }
            set { SetValue(PositionProperty, value); }
        }

        #endregion

        #region  - Methoden (öffentlich) -

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            root = (Grid)GetTemplateChild("root");
            if (root == null)
                return;

            content = (ContentPresenter)GetTemplateChild(PART_Content);
            showStoryBoard = (Storyboard)GetTemplateChild("ShowStoryboard");
            hideStoryboard = (Storyboard)GetTemplateChild("HideStoryboard");
            hideFrame = (EasingDoubleKeyFrame)GetTemplateChild("hideFrame");
            hideFrameY = (EasingDoubleKeyFrame)GetTemplateChild("hideFrameY");
            showFrame = (EasingDoubleKeyFrame)GetTemplateChild("showFrame");
            showFrameY = (EasingDoubleKeyFrame)GetTemplateChild("showFrameY");

            if (hideFrame == null || showFrame == null || hideFrameY == null || showFrameY == null)
                return;

            ApplyAnimation(Position);
        }

        #endregion

        #region  - Methoden (privat) -

        private static Tuple<AppTheme, Accent> DetectTheme(Flyout flyout)
        {
            if (flyout == null)
                return null;

            // first look for owner
            var window = flyout.TryFindParent<MetroWindow>();
            Tuple<AppTheme, Accent> theme = window != null ? ThemeManager.DetectAppStyle(window) : null;
            if (theme != null && theme.Item2 != null)
                return theme;

            // second try, look for main window
            if (Application.Current != null)
            {
                var mainWindow = Application.Current.MainWindow as MetroWindow;
                theme = mainWindow != null ? ThemeManager.DetectAppStyle(mainWindow) : null;
                if (theme != null && theme.Item2 != null)
                    return theme;

                // oh no, now look at application resource
                theme = ThemeManager.DetectAppStyle(Application.Current);
                if (theme != null && theme.Item2 != null)
                    return theme;
            }
            return null;
        }

        private static void PositionChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
        {
            var flyout = (Flyout)dependencyObject;
            bool wasOpen = flyout.ElementSize != ElementSize.Collapsed;
            if (wasOpen && flyout.AnimateOnPositionChange)
            {
                flyout.ApplyAnimation((Position)e.NewValue);
                VisualStateManager.GoToState(flyout, VisualStates.StateCollapsed, true);
            }
            else
            {
                flyout.ApplyAnimation((Position)e.NewValue, false);
            }

            if (wasOpen && flyout.AnimateOnPositionChange)
            {
                flyout.ApplyAnimation((Position)e.NewValue);
                VisualStateManager.GoToState(flyout, VisualStates.StateMaximized, true);
            }
        }

        internal void ChangeFlyoutTheme(Accent windowAccent, AppTheme windowTheme)
        {
            ThemeManager.ChangeAppStyle(Resources, windowAccent, windowTheme);
            SetResourceReference(BackgroundProperty, "HighlightBrush");
            SetResourceReference(ForegroundProperty, "IdealForegroundColorBrush");
        }

        internal void ApplyAnimation(Position position, bool resetShowFrame = true)
        {
            if (root == null || hideFrame == null || showFrame == null || hideFrameY == null || showFrameY == null)
                return;

            if (Position == Position.Left || Position == Position.Right)
                showFrame.Value = 0;
            if (Position == Position.Top || Position == Position.Bottom)
                showFrameY.Value = 0;

            // I mean, we don't need this anymore, because we use ActualWidth and ActualHeight of the root
            //root.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));

            switch (position)
            {
                default:
                    HorizontalAlignment = HorizontalAlignment.Left;
                    VerticalAlignment = VerticalAlignment.Stretch;
                    hideFrame.Value = -root.ActualWidth;
                    if (resetShowFrame)
                        root.RenderTransform = new TranslateTransform(-root.ActualWidth, 0);
                    break;
                case Position.Right:
                    HorizontalAlignment = HorizontalAlignment.Right;
                    VerticalAlignment = VerticalAlignment.Stretch;
                    hideFrame.Value = root.ActualWidth;
                    if (resetShowFrame)
                        root.RenderTransform = new TranslateTransform(root.ActualWidth, 0);
                    break;
                case Position.Top:
                    HorizontalAlignment = HorizontalAlignment.Stretch;
                    VerticalAlignment = VerticalAlignment.Top;
                    hideFrameY.Value = -root.ActualHeight - 1;
                    if (resetShowFrame)
                        root.RenderTransform = new TranslateTransform(0, -root.ActualHeight - 1);
                    break;
                case Position.Bottom:
                    HorizontalAlignment = HorizontalAlignment.Stretch;
                    VerticalAlignment = VerticalAlignment.Bottom;
                    hideFrameY.Value = root.ActualHeight;
                    if (resetShowFrame)
                        root.RenderTransform = new TranslateTransform(0, root.ActualHeight);
                    break;
            }
        }

        protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
        {
            base.OnRenderSizeChanged(sizeInfo);

            if (HandleRenderSizeChange(sizeInfo))
            {
                if (Position == Position.Left || Position == Position.Right)
                    showFrame.Value = 0;
                if (Position == Position.Top || Position == Position.Bottom)
                    showFrameY.Value = 0;

                switch (Position)
                {
                    default:
                        hideFrame.Value = -root.ActualWidth;
                        break;
                    case Position.Right:
                        hideFrame.Value = root.ActualWidth;
                        break;
                    case Position.Top:
                        hideFrameY.Value = -root.ActualHeight - 1;
                        break;
                    case Position.Bottom:
                        hideFrameY.Value = root.ActualHeight;
                        break;
                }
            }
        }

        private void OnHideCompletedMinimizedActions(object sender, EventArgs eventArgs)
        {
            this.SwitchContentElementSize(ElementSize.Minimized);
            Visibility = Visibility.Visible;
            showStoryBoard.Begin(root);
            this.ElementSize = ElementSize.Minimized;
        }

        private void OnHideCompletedMaximizedActions(object sender, EventArgs eventArgs)
        {
            this.SwitchContentElementSize(ElementSize.Maximized);
            Visibility = Visibility.Visible;
            showStoryBoard.Begin(root);
            this.ElementSize = ElementSize.Maximized;
        }

        private void OnHideCompletedCollapsedActions(object sender, EventArgs eventArgs)
        {
            this.SwitchContentElementSize(ElementSize.Collapsed);
            Visibility = Visibility.Collapsed;
            this.ElementSize = ElementSize.Collapsed;
        }

        private void SwitchContentElementSize(ElementSize state)
        {
            foreach (var sizeableElement in this.FindChildren<Control>().OfType<ISizeableElement>())
            {
                sizeableElement.ElementSize = state;
            }

            foreach (var container in this.FindChildren<ContentPresenter>().Where( u => u.Content is ISizeableElement))
            {
                ((ISizeableElement) container.Content).ElementSize = state;
            }
        }

        private bool HandleRenderSizeChange(SizeChangedInfo sizeInfo)
        {
            return ElementSize != ElementSize.Collapsed
                   && (sizeInfo.WidthChanged || sizeInfo.HeightChanged)
                   &&
                   (root != null && hideFrame != null && showFrame != null && hideFrameY != null && showFrameY != null);
        }

        private void UpdateFlyoutTheme()
        {
            if (DesignerProperties.GetIsInDesignMode(this))
            {
                Visibility = this.TryFindParent<FlyoutContainer>() != null ? Visibility.Collapsed : Visibility.Visible;
            }

            var window = this.TryFindParent<MetroWindow>();
            if (window != null)
            {
                Tuple<AppTheme, Accent> windowTheme = DetectTheme(this);

                if (windowTheme != null && windowTheme.Item2 != null)
                {
                    Accent accent = windowTheme.Item2;

                    ChangeFlyoutTheme(accent, windowTheme.Item1);
                }
            }
        }

        #endregion

        private const string PART_Content = "PART_Content";
        private const string PART_CloseButton = "PART_CloseButton";
        private const string PART_MINButton = "PART_MINButton";
        private const string PART_MAXButton = "PART_MAXButton";



    }
}